/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 *  The greatest achievement is selflessness. The greatest worth is self-mastery.
 *  The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 *  The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 *  The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 *  The greatest goodness is a peaceful mind. The greatest patience is humility.
 *  The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 *  The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 * 
 * @author Ashin Wimalajeewa (ash)
 * 
 * acknowledge:
 *  - GWT rpc subsystem developers
 */
package org.javaongems.user.jrpc.impl;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter;
import com.google.gwt.user.server.rpc.impl.ServerSerializableTypeOracle;

public class ClientSerializationStreamWriter extends AbstractSerializationStreamWriter {
	private HashMap/*<Integer, Integer>*/ objectMap = new HashMap/*<Integer, Integer>*/();
	private HashMap/*<String, Integer>*/ stringMap = new HashMap/*<String, Integer>*/();
	private StringBuffer encodeBuffer;
	private int objectCount;
	private ServerSerializableTypeOracle serializableTypeOracle;
	private ArrayList/*<String>*/ stringTable = new ArrayList/*<String>*/();

	private static void append(StringBuffer sb, String token) {
		assert (token != null);
		sb.append(token);
		sb.append('\uffff');
	}

	public ClientSerializationStreamWriter(ServerSerializableTypeOracle serializableTypeOracle) {
		this.serializableTypeOracle = serializableTypeOracle;
	}

	/**
	 * Call this method before attempting to append any tokens. This method
	 * implementation <b>must</b> be called by any overridden version.
	 */
	public void prepareToWrite() {
		objectCount = 0;
		stringTable.clear();
		encodeBuffer = new StringBuffer();
	}

	public String toString() {
		StringBuffer buffer = new StringBuffer();
		writeHeader(buffer);
		writeStringTable(buffer);
		writePayload(buffer);
		return buffer.toString();
	}

	protected int addString(String string) {
		if (string == null) {
			return 0;
		}

		int index = getIntForString(string);
		if (index > 0) 
			return index;
		stringTable.add(string);
		// index is 1-based (that's why we're taking the size AFTER add)
		index = stringTable.size();
		setIntForString(string, index);
		return index;
	}

	/**
	 * Appends a token to the end of the buffer.
	 */
	protected void append(String token) {
		append(encodeBuffer, token);
	}

	protected int getIndexForObject(Object instance) {
		return getIntForInt(System.identityHashCode(instance));
	}

	protected String getObjectTypeSignature(Object instance) {
		if (shouldEnforceTypeVersioning()) {
			return serializableTypeOracle.encodeSerializedInstanceReference(instance.getClass());
		} else {
			return serializableTypeOracle.getSerializedTypeName(instance.getClass());
		}
	}

	protected void saveIndexForObject(Object instance) {
		setIntForInt(System.identityHashCode(instance), objectCount++);
	}

	private int getIntForInt(int key) {
		Integer ret = (Integer) objectMap.get(new Integer(key));
		if (ret == null)
			return -1;
		return ret.intValue();
	}

	private int getIntForString(String key) {
		Integer ret = (Integer) stringMap.get(key);
		if (ret == null)
			return 0;
		return ret.intValue();
	}

	private void setIntForInt(int key, int value) {
		objectMap.put(new Integer(key), new Integer(value));
	}

	private void setIntForString(String key, int value) {
		stringMap.put(key, new Integer(value));
	}

	private void writeHeader(StringBuffer buffer) {
		append(buffer, String.valueOf(SERIALIZATION_STREAM_VERSION));
		append(buffer, String.valueOf(getFlags()));
	}

	private void writePayload(StringBuffer buffer) {
		buffer.append(encodeBuffer.toString());
	}

	private StringBuffer writeStringTable(StringBuffer buffer) {
		int stringTableSize = stringTable.size();
		append(buffer, String.valueOf(stringTableSize));
		for (int i = 0; i < stringTableSize; ++i) {
			append(buffer, (String) stringTable.get(i));
		}
		return buffer;
	}

	protected void serialize(Object instance, String typeSignature)
			throws SerializationException {
		serializeImpl(instance, instance.getClass());
	}

	private void serializeClass(Object instance, Class instanceClass)
			throws SerializationException {
		assert (instance != null);

		Field[] declFields = instanceClass.getDeclaredFields();
		Field[] serializableFields = serializableTypeOracle.applyFieldSerializationPolicy(declFields);
		for (int index = 0; index < serializableFields.length; ++index) {
			Field declField = serializableFields[index];
			assert (declField != null);

			boolean isAccessible = declField.isAccessible();
			boolean needsAccessOverride = !isAccessible && !Modifier.isPublic(declField.getModifiers());
			if (needsAccessOverride) {
				// Override the access restrictions
				declField.setAccessible(true);
			}

			Object value;
			try {
				value = declField.get(instance);
				serializeValue(value, declField.getType());
			} catch (IllegalArgumentException e) {
				throw new SerializationException(e);
			} catch (IllegalAccessException e) {
				throw new SerializationException(e);
			}

			if (needsAccessOverride) {
				// Restore the access restrictions
				declField.setAccessible(isAccessible);
			}
		}

		Class superClass = instanceClass.getSuperclass();
		if (superClass != null && serializableTypeOracle.isSerializable(superClass)) {
			serializeImpl(instance, superClass);
		}
	}

	private void serializeImpl(Object instance, Class instanceClass)
			throws SerializationException {

		assert (instance != null);
		Class customSerializer = serializableTypeOracle.hasCustomFieldSerializer(instanceClass);
		if (customSerializer != null) {
			serializeWithCustomSerializer(customSerializer, instance, instanceClass);
		} else {
			// Arrays are serialized using custom serializers so we should never
			// get
			// here for array types.
			//
			assert (!instanceClass.isArray());
			serializeClass(instance, instanceClass);
		}
	}

	private void serializeWithCustomSerializer(Class customSerializer,
			Object instance, Class instanceClass) throws SerializationException {
		Method serialize;
		try {
			if (instanceClass.isArray()) {
				Class componentType = instanceClass.getComponentType();
				if (!componentType.isPrimitive()) {
					instanceClass = Class.forName("[Ljava.lang.Object;");
				}
			}

			serialize = customSerializer.getMethod("serialize", new Class[] {SerializationStreamWriter.class, instanceClass });
			serialize.invoke(null, new Object[] { this, instance });
		} catch (SecurityException e) {
			throw new SerializationException(e);
		} catch (NoSuchMethodException e) {
			throw new SerializationException(e);
		} catch (IllegalArgumentException e) {
			throw new SerializationException(e);
		} catch (IllegalAccessException e) {
			throw new SerializationException(e);
		} catch (InvocationTargetException e) {
			throw new SerializationException(e);
		} catch (ClassNotFoundException e) {
			throw new SerializationException(e);
		}
	}

	public void serializeValue(Object value, Class type)
			throws SerializationException {
		if (type == boolean.class) {
			writeBoolean(((Boolean) value).booleanValue());
		} else if (type == byte.class) {
			writeByte(((Byte) value).byteValue());
		} else if (type == char.class) {
			writeChar(((Character) value).charValue());
		} else if (type == double.class) {
			writeDouble(((Double) value).doubleValue());
		} else if (type == float.class) {
			writeFloat(((Float) value).floatValue());
		} else if (type == int.class) {
			writeInt(((Integer) value).intValue());
		} else if (type == long.class) {
			writeLong(((Long) value).longValue());
		} else if (type == short.class) {
			writeShort(((Short) value).shortValue());
		} else if (type == String.class) {
			writeString((String) value);
		} else {
			writeObject(value);
		}
	}
}